package networkedmodules;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * <p>
 * A container for all modules in a program. The three constructors for this
 * class allow for host, client, or un-networked use. When networked, a Module
 * in a ModuleGroup can communicate with corresponding connected modules by
 * sending an Update object through its ModuleGroup.
 * </p>
 * 
 * <p>
 * Because ModuleGroups use class names as keys, a ModuleGroup may only contain
 * one instance of any Module
 * </p>
 * 
 * @author Alex Babis
 * 
 */
public final class ModuleGroup
{
    public static final int LOCAL = 0;
    public static final int HOST = 1;
    public static final int CLIENT = 2;
    private int type;

    private Map<String, Module> modules;
    private ModuleGroupConnection connection;
    protected List<Update> updateQueue;

    /**
     * Constructor for local use. Does not create an internet connection. Calls
     * to sendUpdate will do nothing. In this configuration, the only purpose of
     * the module group is so that Modules do not throw null pointers.
     */
    public ModuleGroup()
    {
        modules = new TreeMap<String, Module>();
        type = LOCAL;
        updateQueue = new ArrayList<Update>();
    }

    /**
     * Creates a module group that acts as a host/server. Modules may behave
     * differently if they belong to a server ModuleGroup.
     * 
     * @param portNumber
     *            The port to listen on
     */
    public ModuleGroup(int portNumber)
    {
        modules = new TreeMap<String, Module>();
        try
        {
            connection = new HostModuleGroupConnection(portNumber);
        }
        catch (IOException e)
        {
        }
        connection.setModuleGroup(this);
        type = HOST;
        updateQueue = new ArrayList<Update>();
    }

    /**
     * Creates a module group that acts as a client. Modules may behave
     * differently if they belong to a client ModuleGroup.
     * 
     * @param serverIp
     *            The IP Address of the corresponding server ModuleGroup
     * @param serverPort
     *            The port to listen on
     */
    public ModuleGroup(String serverIp, int serverPort)
    {
        modules = new TreeMap<String, Module>();
        try
        {
            connection = new ClientModuleGroupConnection(serverIp, serverPort);
        }
        catch (IOException e)
        {
        }
        connection.setModuleGroup(this);
        type = CLIENT;
        updateQueue = new ArrayList<Update>();
    }

    /**
     * Adds a Module to this group. Once this is done, the module may send
     * Update objects through the network by invoking the sendUpdate method.
     * Only one instance of a particular Module may be added to a ModuleGroup
     * 
     * @param module
     *            The Module to be added
     */
    public final void addModule(Module module)
    {
        modules.put(module.getKey(), module);
        module.setModuleGroup(this);
    }

    /**
     * Gets the Module corresponding to the given key the key is the class name
     * of the target object.
     * 
     * @param key
     *            A String equal to the class name of the target Module
     * @return
     */
    protected final Module getModule(String key)
    {
        return modules.get(key);
    }

    /**
     * Sends an Update object through the connection. This method should not be
     * invoked if the ModuleGroup is non-networked.
     * 
     * @param update
     */
    public final void sendUpdate(Update update)
    {
        connection.sendUpdate(update);
    }

    /**
     * Gets an integer corresponding to the ModuleGroup's connection type.
     * 
     * @return An int corresponding to the connection type.
     */
    public int getType()
    {
        return type;
    }

    /*------------------
     * PRIVATE CLASSES *
     *-----------------*/
    private abstract class ModuleGroupConnection implements Runnable
    {
        protected ModuleGroup group;

        public void setModuleGroup(ModuleGroup group)
        {
            this.group = group;
        }

        public abstract void sendUpdate(Update update);

        public abstract void close();
    }

    private class HostModuleGroupConnection extends ModuleGroupConnection
    {
        ArrayList<HostConnectionHandler> connections = null;
        ServerSocket serverSocket = null;

        boolean allowingNewUsers = true;

        public HostModuleGroupConnection(int portNumber) throws IOException
        {
            connections = new ArrayList<HostConnectionHandler>();
            serverSocket = new ServerSocket(portNumber);
            (new Thread(this)).start();
        }

        public void sendUpdate(Update update)
        {
            System.out.println("Host sending update: " + update);
            synchronized (connections)
            {
                for (HostConnectionHandler connection : connections)
                    try
                    {
                        connection.out.reset();
                        connection.out.writeObject(update);
                    }
                    catch (IOException e)
                    {
                    }
            }
        }

        @Override
        public void run()
        {
            while (allowingNewUsers)
            {
                try
                {
                    HostConnectionHandler connection = new HostConnectionHandler(
                            serverSocket.accept());
                    synchronized (connections)
                    {
                        connections.add(connection);
                    }
                    (new Thread(connection)).start();
                }
                catch (IOException e)
                {
                }
            }
        }

        private class HostConnectionHandler implements Runnable
        {
            Socket socket;
            ObjectOutputStream out = null;
            ObjectInputStream in = null;

            public HostConnectionHandler(Socket socket) throws IOException
            {
                this.socket = socket;
                out = new ObjectOutputStream(socket.getOutputStream());
                in = new ObjectInputStream(socket.getInputStream());
                out.flush();
            }

            public void run()
            {
                while (true)
                {
                    try
                    {
                        Update update = (Update) in.readObject();
                        System.out.println("Host received:" + update);
                        update.performUpdate(group);
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                        try
                        {
                            socket.close();
                            break;
                        }
                        catch (IOException e1)
                        {
                        }
                    }
                    catch (ClassNotFoundException e)
                    {
                        e.printStackTrace();
                    }
                }
            }
        }

        @Override
        protected void finalize()
        {
            try
            {
                super.finalize();
            }
            catch (Throwable e1)
            {
            }
            for (HostConnectionHandler connection : connections)
            {
                try
                {
                    connection.in.close();
                    connection.out.close();
                    connection.socket.close();
                }
                catch (IOException e)
                {
                }
            }
            try
            {
                serverSocket.close();
            }
            catch (IOException e)
            {
            }
        }

        @Override
        public void close()
        {
            for (HostConnectionHandler h : connections)
            {

                try
                {
                    h.in.close();
                    h.out.close();
                    h.socket.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    private class ClientModuleGroupConnection extends ModuleGroupConnection
    {
        Socket socket;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;

        public ClientModuleGroupConnection(String serverIp, int serverPort)
                throws IOException
        {
            socket = new Socket(serverIp, serverPort);
            out = new ObjectOutputStream(socket.getOutputStream());
            in = new ObjectInputStream(socket.getInputStream());
            out.flush();
            (new Thread(this)).start();
        }

        public void sendUpdate(Update update)
        {
            System.out.println("Sending update: " + update);
            try
            {
                out.reset();
                out.writeObject(update);
            }
            catch (IOException e)
            {
            }
        }

        public void run()
        {
            while (true)
            {
                try
                {
                    // TODO: Check for Update class type
                    Update update = (Update) in.readObject();
                    if (update == null)
                    {
                        System.err.println("Object Stream Received a null");
                        continue;
                    }
                    System.out.println("Client received:" + update);
                    update.performUpdate(group);
                }
                catch (ClassNotFoundException e)
                {
                }
                catch (IOException e)
                {
                    try
                    {
                        socket.close();
                        break;
                    }
                    catch (IOException e1)
                    {
                    }
                }
            }
        }

        protected void finalize()
        {
            try
            {
                super.finalize();
            }
            catch (Throwable e1)
            {
            }
            try
            {
                in.close();
                out.close();
                socket.close();
            }
            catch (IOException e)
            {
            }
        }

        @Override
        public void close()
        {
            try
            {
                in.close();
                out.close();
                socket.close();
            }
            catch (IOException e)
            {
            }
        }
    }

    public void kill()
    {
        if (connection != null)
            connection.close();
    }
}
